﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Mindfor.Web.Config;
using Mindfor.Web.Data;
using NHibernate;
using NHibernate.Context;
using System.Data.SqlClient;
using System.Reflection;

namespace Mindfor.Web
{
	/// <summary>
	/// Defines methods and properties that are common for MindforCms working proccess.
	/// </summary>
	public abstract class CmsApplication : HttpApplication, IDisposable
	{
		/// <summary>
		/// Template string for error message. Can be loaded from file, if specified in web.config.
		/// </summary>
		private static string s_errorMessageTemplate;

		/// <summary>
		/// Is template string for error message loaded.
		/// </summary>
		private static bool s_errorMessageTemplateLoaded;

		/// <summary>
		/// Contains information about exception occured at ApplicationStart.
		/// </summary>
		private static Exception s_appStartException;

		/// <summary>
		/// Gets data provider.
		/// </summary>
		public static IDataProvider DataProvider { get; private set; }

		HttpContextBase m_context;

		/// <summary>
		/// Gets http context.
		/// </summary>
		public new HttpContextBase Context
		{
			get
			{
				if (m_context == null && base.Context != null)
					m_context = new HttpContextWrapper(base.Context);
				return m_context;
			}
		}

		/// <summary>
		/// Restarts current web-application.
		/// </summary>
		public static void Restart()
		{
			HttpRuntime.UnloadAppDomain();
		}

		/// <summary>
		/// Registers application routes.
		/// </summary>
		protected virtual void RegisterRoutes(RouteCollection routes)
		{
			routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
		}

		/// <summary>
		/// Registers global filters.
		/// </summary>
		/// <param name="filters">Filter collection that filters must be added to.</param>
		protected virtual void RegisterGlobalFilters(GlobalFilterCollection filters)
		{
			filters.Add(new HttpStatusCodeReplaceAttribute());
			filters.Add(new AdminMasterReplaceFilter());
		}

		#region Event handlers

		/// <summary>
		/// Handles the application start event.
		/// </summary>
		protected virtual void Application_Start()
		{
			try
			{
				// create data provider
				DataProvider = CmsConfig.Default.DataProvider.CreateInstance();

				// register globals
				DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
				ModelBinders.Binders.DefaultBinder = new CmsModelBinder();
				ControllerBuilder.Current.SetControllerFactory(typeof(CmsControllerFactory));
				ViewEngines.Engines.Clear();
				ViewEngines.Engines.Add(new CmsRazorViewEngine());
				ViewEngines.Engines.Add(new WebFormViewEngine());
				RegisterGlobalFilters(GlobalFilters.Filters);
				
				// register routes
				RegisterRoutes(RouteTable.Routes);

				// register modules
				using (ISession data = DataProvider.SessionFactory.OpenSession())
				{
					ModuleFactory.Default.LoadIsEnabled(data);
					ModuleFactory.Default.RegisterEnabledModules(data);
				}
			}
			catch (Exception ex)
			{
				s_appStartException = ex;
			}
		}

		/// <summary>
		/// Handles the application end event.
		/// </summary>
		protected virtual void Application_End()
		{
			ModuleFactory.Default.UnloadRegisteredModules();
		}

		/// <summary>
		/// Handles the application begin request event.
		/// </summary>
		protected virtual void Application_BeginRequest()
		{
			if (DataProvider != null)
			{
				// register data session
				if (!CurrentSessionContext.HasBind(DataProvider.SessionFactory))
					CurrentSessionContext.Bind(DataProvider.SessionFactory.OpenSession());
			}
		}

		/// <summary>
		/// Handles the application end request event.
		/// </summary>
		protected virtual void Application_EndRequest()
		{
			if (DataProvider != null)
			{
				// unregister data session
				ISession data = CurrentSessionContext.Unbind(DataProvider.SessionFactory);
				if (data != null)
				{
					data.Close();
					data.Dispose();
				}
			}
		}

		/// <summary>
		/// Handles the application post map request handler event.
		/// </summary>
		protected virtual void Application_PostMapRequestHandler(object sender, EventArgs e)
		{
			if (Context.Handler is DefaultHttpHandler || Context.Handler is MvcHandler)
			{
				if (s_appStartException != null)
				{
					Restart();
					throw new ApplicationException("Exception occured on application start.", s_appStartException);
				}
			}
		}

		/// <summary>
		/// Handles post request handler execute event. Conmpress server output using gzip or deflate.
		/// </summary>
		protected virtual void Application_PostRequestHandlerExecute(object sender, EventArgs e)
		{
			HttpApplication app = sender as HttpApplication;
			if (app == null)
				return;

			// check status code
			//if (app.Response.StatusCode == 401 && !app.User.Identity.IsAuthenticated)
			//	RedirectToLoginPage();
			
			// gzip and deflate compression
			string acceptEncoding = app.Request.Headers["Accept-Encoding"];
			if (app.Request["HTTP_X_MICROSOFTAJAX"] == null && !String.IsNullOrEmpty(acceptEncoding))
			{
				// check handler & encoding
				string contentType = app.Response.ContentType;
				string[] compressedStaticContentTypes = new string[] { "text/css", "application/x-javascript", "application/javascript" };
				if (CmsConfig.Default.EnableDynamicCompression && contentType == "text/html" ||
					CmsConfig.Default.EnableStaticCompression && compressedStaticContentTypes.Contains(contentType))
				{
					acceptEncoding = acceptEncoding.ToLower();
					Stream uncompressedStream = app.Response.Filter;					
					if (acceptEncoding.Contains("gzip") || acceptEncoding == "*")
					{
						// gzip
						app.Response.Filter = new GZipStream(uncompressedStream, CompressionMode.Compress);
						app.Response.AppendHeader("Content-Encoding", "gzip");
					}
					else if (acceptEncoding.Contains("deflate"))
					{
						// default
						app.Response.Filter = new DeflateStream(uncompressedStream, CompressionMode.Compress);
						app.Response.AppendHeader("Content-Encoding", "deflate");
					}
				}
			}
		}

		/// <summary>
		/// Handles the application error event.
		/// </summary>
		protected virtual void Application_Error(object sender, EventArgs e)
		{
			Exception ex = Server.GetLastError();
			if (ex is HttpUnhandledException && ex.InnerException != null)
				ex = ex.InnerException;

			// log to mindfor server
			if (CmsConfig.Default.ApplicationId.HasValue)
				Mindfor.Error.ErrorProxy.SubmitSafe(CmsConfig.Default.ApplicationId.Value, ex);
		
			// log local
			try
			{
				using (ISession data = DataProvider.SessionFactory.OpenSession())
				{
					using (ITransaction t = data.BeginTransaction())
					{
						Log log = new Log(ex);
						data.Save(log);
						t.Commit();
					}
				}
			}
			catch { }
			
			// custom html error
			if (!(ex is CmsConfigException) && CmsConfig.Default.EnableErrorMessage)
			{
				Server.ClearError();
				string message = CreateSystemErrorHtml(ex);
				WriteErrorMessageToResponse(message);
			}
		}

		#endregion

		#region Custom error

		/// <summary>
		/// Creates system error message for exception and all it's inner exceptions.
		/// </summary>
		/// <param name="ex">Exception to handle.</param>
		public virtual string CreateSystemErrorHtml(Exception ex)
		{
			// write header
			StringBuilder sb = new StringBuilder();

			// write message
			SqlException sqlEx = ex as SqlException;
			if (sqlEx != null && sqlEx.Class >= 20)
			{
				sb.Append("<p>Cannot connect to the database.</p>");
				sb.Append("<p><b>The reason is: </b>");
				sb.Append(sqlEx.Message);
				sb.Append("</p>");
			}
			else
			{
				int i = 0;
				while (ex != null)
				{
					if (i == 0)
						sb.Append("<h2>Exception info</h2>");
					else
						sb.Append("<h2>Inner exception " + i + "</h2>");

					sb.Append(CreateFriendlyErrorMessageHtml(ex));
					ex = ex.InnerException;
					i++;
				}
			}

			// write signature
			Assembly asm = Assembly.GetExecutingAssembly();
			Version ver = asm.GetName().Version;
			string aspNetVersion = asm.ImageRuntimeVersion;
			sb.Append("<br/><hr/><p>This message is generated by Mindfor WebMP v");
			sb.Append(ver.Major + "." + ver.Minor + " revision " + ver.Revision + " build " + ver.Build + " (based on Mindfor CMS v4.1)");
			sb.Append("<br/>Executed in ASP.NET " + aspNetVersion + "</p>");

			// return
			return sb.ToString();
		}

		/// <summary>
		/// Creates friendly error message for one exception.
		/// </summary>
		/// <param name="ex">Exception to create message for.</param>
		/// <returns>Friendly error message.</returns>
		protected virtual string CreateFriendlyErrorMessageHtml(Exception ex)
		{
			StringBuilder sb = new StringBuilder();

			sb.Append("<p><b>Type:</b> ");
			sb.Append(ex.GetType().FullName);
			sb.Append("</p>");

			sb.Append("<p><b>Message:</b> ");
			sb.Append(ex.Message);
			sb.Append("</p>");

			if (!String.IsNullOrEmpty(ex.Source))
			{
				sb.Append("<p><b>Source:</b> ");
				sb.Append(ex.Source);
				sb.Append("</p>");
			}

			if (!String.IsNullOrEmpty(ex.StackTrace))
			{
				string stack = ex.StackTrace.Replace("\r\n", "\r\n<br/>");
				sb.Append("<p><b>Stack trace:</b><br/>");
				sb.Append(stack);
				sb.Append("</p>");
			}

			return sb.ToString();
		}

		/// <summary>
		/// Writes error message to response.
		/// </summary>
		/// <param name="htmlMessage">Error message html text.</param>
		private void WriteErrorMessageToResponse(string htmlMessage)
		{
			// try to write
			HttpResponse res = HttpContext.Current.Response;
			res.ClearContent();

			// if we have error message template file and haven't read it already
			if (s_errorMessageTemplate == null && !String.IsNullOrEmpty(CmsConfig.Default.ErrorDesignLayoutName))
			{
				// so we read it to static var
				try
				{
					FileInfo templateFile = new FileInfo(Server.MapPath("~/Views/Shared/" + CmsConfig.Default.ErrorDesignLayoutName));
					if (templateFile.Exists)
					{
						using (StreamReader sr = templateFile.OpenText())
							s_errorMessageTemplate = sr.ReadToEnd();
					}
				}
				catch { }
				finally
				{
					s_errorMessageTemplateLoaded = true;
				}
			}

			// next we form the final error message
			if (!String.IsNullOrEmpty(s_errorMessageTemplate))
			{
				int start = s_errorMessageTemplate.IndexOf(CmsConstants.ErrorMessagePlaceHolder);
				if (start >= 0)
				{
					htmlMessage = s_errorMessageTemplate.Substring(0, start) + htmlMessage
								+ s_errorMessageTemplate.Substring(start + CmsConstants.ErrorMessagePlaceHolder.Length);
				}
			}

			// write and close response
			res.Write(htmlMessage);
			res.End();
		}

		#endregion
	}
}
